use chrono::Local;
use pbr::ProgressBar;
use ssh2::{Channel, ErrorCode, OpenFlags, Session};
use std::io::prelude::*;
use std::{fs::File, net::TcpStream, path::PathBuf, process::exit, time::Duration};

const SSH_TOTAL_RETRY_COUNT: usize = 10;
pub const PACKAGE_NAME: &str = env!("CARGO_PKG_NAME");
const CHUNK_SIZE: usize = 16 * 1024;
const LONG_CHUNK_SIZE: usize = 1024 * 1024;
// const MAX_TERM_COLUMNS: u16 = 256;
// const DEF_TERM_COLUMNS: usize = 50;

#[derive(Debug, Default, Clone)]
pub struct Server {
    pub hostname: String,
    pub port: String,
    pub username: String,
    pub password: String,
}

impl Server {
    // pub fn connect(&self){
    //     connect_ssh(&self)
    // }
}

#[inline]
fn get_filesize(file: &PathBuf) -> u64 {
    std::fs::metadata(file)
        .map(|metadata| metadata.len())
        .unwrap_or(0)
}

pub fn connect_ssh(s: &Server) -> Session {
    // 新建连接
    let tcp = match TcpStream::connect(format!("{}:{}", s.hostname, s.port)) {
        Ok(tcp) => tcp,
        Err(e) => {
            // 无法链接到对应的端口
            eprintln!(
                "Host: {}:{}, Session create failed, Cause: {}",
                s.hostname, s.port, e
            );
            exit(1)
        }
    };
    let mut sess = Session::new().unwrap();
    sess.set_tcp_stream(tcp);
    match sess.handshake() {
        Ok(()) => {}
        Err(e) => {
            eprintln!(
                "Host: {}:{}, Server handshake failed, cause: {}",
                s.hostname, s.port, e
            );
            exit(1)
        }
    }
    let pwd = &s.password.clone();
    match sess.userauth_password(&s.username, pwd) {
        Ok(()) => {}
        Err(e) => {
            eprintln!(
                "Host: {}:{}, Server auth failed, cause: {}",
                s.hostname, s.port, e
            );
            exit(1)
        }
    }
    println!("Connected to {}@{}:{}", s.username, s.hostname, s.port);
    if let Err(e) = sess.set_banner("monica") {
        eprintln!(
            "Update server {}:{} set_banner error, cause: {}",
            s.hostname, s.port, e
        );
    }
    //
    // sess.set_keepalive(true, SSH_KEEPALIVE_INTERVAL as u32);

    sess
}

#[inline]
fn pb(file_size: u64, file_name: &str) -> ProgressBar<std::io::Stdout> {
    let mut pb = ProgressBar::new(file_size);
    pb.set_units(pbr::Units::Bytes);
    pb.format("╢▌▌░╟");
    pb.message(&format!("{}        ", file_name));
    // pb.tick_format("▀▐▄▌");
    pb
}

fn close(mut ch: Channel, s: &Server, file: PathBuf, rfile: PathBuf) {
    let remote_file_name = rfile.file_name().unwrap().to_str().unwrap();

    let mut total_try_count = 0;
    if let Err(e) = ch.send_eof() {
        eprintln!("Channel send EOF: cause: {}", e);
        total_try_count = SSH_TOTAL_RETRY_COUNT;
    }
    if let Err(e) = ch.wait_eof() {
        eprintln!("Channel wait EOF error: cause: {}", e);
        total_try_count = SSH_TOTAL_RETRY_COUNT;
    }
    if let Err(e) = ch.close() {
        eprintln!("Channel close error: cause: {}", e);
        total_try_count = SSH_TOTAL_RETRY_COUNT;
    }
    if let Err(e) = ch.wait_close() {
        eprintln!("Channel wait close error: cause: {}", e);
        total_try_count = SSH_TOTAL_RETRY_COUNT;
    }

    let mut try_count = 0;
    while try_count < total_try_count {
        let sess = connect_ssh(&s);
        scp_send(&sess, s.clone(), file.clone(), rfile.clone());

        eprintln!(
            "Download file {} interrupted, Network not available, [{}/{}] retry again after 3s.",
            remote_file_name,
            try_count + 1,
            total_try_count
        );
        // 休眠3秒，重试
        std::thread::sleep(Duration::from_secs(3));
        try_count += 1;
        if try_count == 10 {
            // 超过重试次数
            // 网络异常
            println!("ERROR: Network not available, exceeding retry attempts, exit now.");
            exit(-1);
        }
    }
}

// 向远程服务器发送文件
pub fn scp_send(sess: &Session, s: Server, file: PathBuf, rfile: PathBuf) {
    let local_file_name = file.file_name().unwrap().to_str().unwrap();

    // 获取本地文件的基础信息
    let file_size = get_filesize(&file);
    let remote_sftp = sess.sftp().unwrap();

    // 目标是目录
    let remote_tmp_file = match remote_sftp.opendir(&rfile) {
        Ok(_) => rfile.join(local_file_name),
        Err(_) => rfile.clone(),
    };

    if let Err(e) = remote_sftp.open_mode(
        &remote_tmp_file,
        OpenFlags::READ | OpenFlags::WRITE | OpenFlags::CREATE,
        0o644,
        ssh2::OpenType::File,
    ) {
        match e.code() {
            ErrorCode::SFTP(2) => {
                println!(
                    "{PACKAGE_NAME}: {} -> {}: no such file or directory",
                    file.display(),
                    remote_tmp_file.display()
                );
            }
            _ => {
                println!(
                    "{PACKAGE_NAME}: {} -> {}: {}",
                    file.display(),
                    remote_tmp_file.display(),
                    e.message()
                );
            }
        }
        exit(255);
    }

    // 打开远程文件
    let mut ch;
    loop {
        match sess.scp_send(&remote_tmp_file, 0o644, file_size, None) {
            Ok(c) => {
                ch = c;
                break;
            }
            Err(e) => {
                // 其他问题
                eprintln!(
                    "{PACKAGE_NAME}: {:?}: {}, retry again, sleep 3s.",
                    remote_tmp_file.display(),
                    e.message()
                );
                std::thread::sleep(Duration::from_secs(3));
            }
        };
    }

    // 读取本地文件
    let mut f;
    loop {
        match File::open(&file) {
            Ok(file) => {
                f = file;
                break;
            }
            Err(e) => {
                println!(
                    "{PACKAGE_NAME}: {:?}: {}, retry again, sleep 3s.",
                    local_file_name, e
                );
            }
        }
        std::thread::sleep(Duration::from_secs(3));
    }

    // 开始写入
    let mut buf: Vec<u8> = Vec::new();
    f.read_to_end(&mut buf).unwrap();

    let mut pb = pb(file_size as u64, local_file_name);
    
    // 16KB
    // 1.5M => 1M
    for chunk in buf.chunks(if file_size >= 1048576 {
        LONG_CHUNK_SIZE
    } else {
        CHUNK_SIZE
    }) {
        // called `Result::unwrap()` on an `Err` value: Custom { kind: Other, error: "Unable to send channel data" }
        // 网络不稳定会导致报错：error: "Unable to send channel data"
        match ch.write_all(chunk) {
            Err(_) => {
                // 再次重试还是失败的话，那只能重新上传整个文件
                match ch.write_all(chunk) {
                    Err(e) => {
                        eprintln!(
                            "{PACKAGE_NAME}: {} interrupted, Network not available, {}",
                            local_file_name, e
                        );
                        break;
                    }
                    _ => {}
                }
            }
            _ => {}
        }

        pb.add(chunk.len() as u64);

    }

    pb.finish_println(&format!(
        "{} -> {}@{}:{} completed\n",
        file.display(),
        s.username,
        s.hostname,
        rfile.display()
    ));

    close(ch, &s, file, rfile);
}

// 向远程服务器读取文件
pub fn scp_recv(sess: &Session, s: Server, file: PathBuf, rfile: PathBuf) {
    let remote_file_name = rfile.file_name().unwrap().to_str().unwrap();
    let (mut remote_file, stat) = match sess.scp_recv(rfile.as_path()) {
        Ok((a, b)) => (a, b),
        Err(e) => {
            match e.code() {
                ErrorCode::SFTP(2) => {
                    println!(
                        "{PACKAGE_NAME}: {} -> {}: no such file or directory",
                        rfile.display(),
                        file.display(),
                    );
                }
                _ => {
                    println!(
                        "{PACKAGE_NAME}: {} -> {}: {}",
                        rfile.display(),
                        file.display(),
                        e.message()
                    );
                }
            }
            exit(1);
        }
    };

    // 获取远程文件的基础信息
    let file_size = stat.size();

    // 目标是目录
    let local_tmp_file = if file.is_dir() {
        file.join(remote_file_name)
    } else {
        file.clone()
    };

    // 尝试生成文件，如果失败，直接退出
    let mut fd = match std::fs::OpenOptions::new()
        .read(true)
        .write(true)
        .create(true)
        .open(&local_tmp_file)
    {
        Err(e) => {
            println!(
                "{PACKAGE_NAME}: {} -> {}: {}",
                rfile.display(),
                local_tmp_file.display(),
                e
            );
            exit(255);
        }
        Ok(f) => f,
    };

    let mut pb = pb(file_size as u64, remote_file_name);
    let mut buf = [0; CHUNK_SIZE];

    loop {
        let n = match remote_file.read(&mut buf[..]) {
            Ok(n) => n,
            Err(e) => {
                println!(
                    "{PACKAGE_NAME}: {} -> {}: {}",
                    rfile.display(),
                    local_tmp_file.display(),
                    e
                );
                exit(255);
            }
        };

        let n = match fd.write(&buf[0..n]) {
            Err(e) => {
                println!(
                    "{PACKAGE_NAME}: {} -> {}: {}",
                    rfile.display(),
                    local_tmp_file.display(),
                    e
                );
                exit(255);
            }
            Ok(n) => n,
        };
        if n < 16384 {
            break;
        }

        pb.add(n as u64);
    }

    pb.finish_println(&format!(
        "{}@{}:{} -> {} completed\n",
        s.username,
        s.hostname,
        rfile.display(),
        file.display()
    ));

    close(remote_file, &s, file, rfile);
}

// 执行远程命令
pub fn exec_cmd(sess: &Session, cmd: &str, timing: bool) {
    let now = Local::now();
    let mut channel = sess.channel_session().unwrap();
    channel.exec(&cmd).unwrap();

    let mut stdout = String::new();
    channel.read_to_string(&mut stdout).unwrap();
    let mut stderr: String = String::new();
    channel.stderr().read_to_string(&mut stderr).unwrap();

    channel.wait_close().unwrap();
    // println!("before.info1: cmd: `{}`", &cmd);
    let status = channel.exit_status().unwrap();
    if status == 0 {
        println!("{}", stdout.trim_end_matches("\n"));
    } else {
        println!("{}", stderr.trim_end_matches("\n"));
        exit(status);
    }

    if timing {
        let now = Local::now() - now;
        println!(
            "Elapsed: {:.3} sec",
            (now.num_milliseconds() as f64 / 1000.0) as f64
        );
    }
}
